using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using EnvDTE;
using Microsoft.VisualStudio.Designer.Interfaces;
using VSLangProj;

namespace dotGen.CustomTool.Fx
{
    public abstract class BaseCodeGeneratorWithSite : BaseCodeGenerator, IObjectWithSite
    {
        private const int E_FAIL = unchecked((int)0x80004005);
        private const int E_NOINTERFACE = unchecked((int)0x80004002);

        private object site;
        private CodeDomProvider codeDomProvider;
        private static readonly Guid CodeDomInterfaceGuid = new Guid("{73E59688-C7C4-4a85-AF64-A538754784C5}");
        private static readonly Guid CodeDomServiceGuid = CodeDomInterfaceGuid;
        private ServiceProvider serviceProvider;

        protected virtual CodeDomProvider CodeProvider
        {
            get
            {
                if (codeDomProvider == null)
                {
                    var vsmdCodeDomProvider = (IVSMDCodeDomProvider)GetService(CodeDomServiceGuid);
                    if (vsmdCodeDomProvider != null)
                        codeDomProvider = (CodeDomProvider)vsmdCodeDomProvider.CodeDomProvider;
                    Debug.Assert(codeDomProvider != null, "Get CodeDomProvider Interface failed.  GetService(QueryService(CodeDomProvider) returned Null.");
                }
                return codeDomProvider;
            }
            set
            {
                if (value == null)
                    throw new ArgumentNullException();
                codeDomProvider = value;
            }
        }

        private ServiceProvider SiteServiceProvider
        {
            get
            {
                if (serviceProvider == null)
                {
                    var oleServiceProvider = site as IOleServiceProvider;
                    Debug.Assert(oleServiceProvider != null, "Unable to get IOleServiceProvider from site object.");
                    serviceProvider = new ServiceProvider(oleServiceProvider);
                }
                return serviceProvider;
            }
        }

        protected object GetService(Guid serviceGuid)
        {
            return SiteServiceProvider.GetService(serviceGuid);
        }

        protected object GetService(Type serviceType)
        {
            return SiteServiceProvider.GetService(serviceType);
        }

        public override string GetDefaultExtension()
        {
            CodeDomProvider codeDom = CodeProvider;
            Debug.Assert(codeDom != null, "CodeDomProvider is NULL.");
            string extension = codeDom.FileExtension;
            if (!string.IsNullOrEmpty(extension) && extension[0] != '.')
                extension = "." + extension;
            return extension;
        }

        public virtual void SetSite(object pUnkSite)
        {
            site = pUnkSite;
            codeDomProvider = null;
            serviceProvider = null;
        }

        public virtual void GetSite(ref Guid riid, object[] ppvSite)
        {
            if (ppvSite == null)
                throw new ArgumentNullException("ppvSite");

            if (ppvSite.Length < 1)
                throw new ArgumentException("ppvSite array must have at least 1 member", "ppvSite");

            if (site == null)
                throw new COMException("object is not sited", E_FAIL);

            IntPtr pUnknownPointer = Marshal.GetIUnknownForObject(site);
            IntPtr intPointer;
            Marshal.QueryInterface(pUnknownPointer, ref riid, out intPointer);

            if (intPointer == IntPtr.Zero)
                throw new COMException("site does not support requested interface", E_NOINTERFACE);

            ppvSite[0] = Marshal.GetObjectForIUnknown(intPointer);
        }

        private static string GetDLLNames(string[] DLLToAdd)
        {
            if (DLLToAdd == null || DLLToAdd.Length == 0)
                return string.Empty;

            string dllNames = DLLToAdd[0];
            for (int dllIdx = 1; dllIdx < DLLToAdd.Length; dllIdx++)
                dllNames = dllNames + ", " + DLLToAdd[dllIdx];
            return dllNames;
        }

        protected void AddReferenceDLLToProject(string[] referenceDLL)
        {
            if (referenceDLL.Length == 0)
                return;

            object serviceObject = GetService(typeof(ProjectItem));
            Debug.Assert(serviceObject != null, "Unable to get Project Item.");
            if (serviceObject == null)
            {
                string errorMessage = String.Format("Unable to add DLL to project references: {0}.  Please Add them manually.", GetDLLNames(referenceDLL));
                GeneratorErrorCallback(false, 1, errorMessage, 0, 0);
                return;
            }

            Project containingProject = ((ProjectItem)serviceObject).ContainingProject;
            Debug.Assert(containingProject != null, "GetService(typeof(Project)) return null.");
            if (containingProject == null)
            {
                string errorMessage = String.Format("Unable to add DLL to project references: {0}.  Please Add them manually.", GetDLLNames(referenceDLL));
                GeneratorErrorCallback(false, 1, errorMessage, 0, 0);
                return;
            }

            var vsProj = containingProject.Object as VSProject;
            Debug.Assert(vsProj != null, "Unable to ADD DLL to current project.  Project.Object does not implement VSProject.");
            if (vsProj == null)
            {
                string errorMessage = String.Format("Unable to add DLL to project references: {0}.  Please Add them manually.", GetDLLNames(referenceDLL));
                GeneratorErrorCallback(false, 1, errorMessage, 0, 0);
                return;
            }

            try
            {
                for (int i = 0; i < referenceDLL.Length; i++)
                {
                    vsProj.References.Add(referenceDLL[i]);
                }
            }
            catch (Exception e)
            {
                Debug.Fail("**** ERROR: vsProj.References.Add() throws exception: " + e);

                string errorMessage = String.Format("Unable to add DLL to project references: {0}.  Please Add them manually.", GetDLLNames(referenceDLL));
                GeneratorErrorCallback(false, 1, errorMessage, 0, 0);
                return;
            }
        }

        protected virtual string CreateExceptionMessage(Exception e)
        {
            string message = e.Message ?? string.Empty;

            Exception innerException = e.InnerException;
            while (innerException != null)
            {
                string innerMessage = innerException.Message;
                if (!string.IsNullOrEmpty(innerMessage))
                    message = message + " " + innerMessage;
                innerException = innerException.InnerException;
            }

            return message;
        }

        protected virtual void GenerateVersionComment(System.CodeDom.CodeNamespace codeNamespace)
        {
            codeNamespace.Comments.Add(new CodeCommentStatement(string.Empty));
            codeNamespace.Comments.Add(new CodeCommentStatement(string.Format("This source code was auto-generated by {0}, Version {1}.",
                Assembly.GetExecutingAssembly().GetName().Name, Environment.Version)));
            codeNamespace.Comments.Add(new CodeCommentStatement(string.Empty));
        }
    }
}